export interface Element {
  type: string;
  tagName?: string;
  content?: string;
  children?: Element[];
  style?: Record<string, any>;
  computedStyle?: Record<
    string,
    {
      value: string;
      specificity: [number, number, number, number];
    }
  >;
}

/**
 * Run layout on the render tree to compute geometry of each node
 * @param element Render Tree
 */
export function layout(element: Element) {
  if (element.computedStyle == null || element.children == null) {
    return;
  }

  const style = getStyle(element);

  // only layout for flex elements
  if (style.display !== 'flex') {
    return;
  }

  // flex layout 算法
  // https://www.w3.org/TR/css-flexbox-1/#layout-algorithm

  const items = element.children.filter((e: Element) => {
    return e.type === 'element';
  });

  // sort items by order in style
  items.sort((a, b) => {
    return a.style?.order ?? 0 - b.style?.order ?? 0;
  });

  const sizeProperties = ['width', 'height'];

  for (const size of sizeProperties) {
    if (style[size] === 'auto' || style[size] === '') {
      style[size] = null;
    }
  }

  // https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction
  if (!style['flex-direction'] || style['flex-direction'] === 'auto') {
    style['flex-direction'] = 'row';
  }

  // https://developer.mozilla.org/en-US/docs/Web/CSS/align-items
  // Controls alignment of all items on the cross axis.
  if (!style['align-items'] || style['align-items'] === 'auto') {
    style['align-items'] = 'stretch';
  }

  // https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content
  // Controls alignment of all items on the main axis.
  if (!style['justify-content'] || style['justify-content'] === 'auto') {
    style['justify-content'] = 'flex-start';
  }

  // https://developer.mozilla.org/en-US/docs/Web/CSS/align-content
  // Described in the spec as for “packing flex lines”;
  // Controls space between flex lines on the cross axis.
  if (!style['align-content'] || style['align-content'] === 'auto') {
    style['align-content'] = 'stretch';
  }

  // https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap
  if (!style['flex-wrap'] || style['flex-wrap'] === 'auto') {
    style['flex-wrap'] = 'nowrap';
  }

  // main axis size
  let mainSize: 'width' | 'height';

  // main axis start
  let mainStart: 'left' | 'right' | 'top' | 'bottom';

  // main axis end
  let mainEnd: 'right' | 'left' | 'bottom' | 'top';

  let mainSign: 1 | -1;
  let mainBase: number;

  // cross axis size
  let crossSize: 'width' | 'height';

  // cross axis start
  let crossStart: 'left' | 'right' | 'top' | 'bottom';

  // cross axis end
  let crossEnd: 'right' | 'left' | 'bottom' | 'top';

  let crossSign: 1 | -1;
  let crossBase: number;

  switch (style['flex-direction']) {
    case 'row': {
      mainSize = 'width';
      mainStart = 'left';
      mainEnd = 'right';
      mainSign = 1;
      mainBase = 0;

      crossSize = 'height';
      crossStart = 'top';
      crossEnd = 'bottom';
      break;
    }

    case 'row-reverse': {
      mainSize = 'width';
      mainStart = 'right';
      mainEnd = 'left';
      mainSign = -1;
      mainBase = style[mainSize];

      crossSize = 'height';
      crossStart = 'top';
      crossEnd = 'bottom';
      break;
    }

    case 'column': {
      mainSize = 'height';
      mainStart = 'top';
      mainEnd = 'bottom';
      mainSign = 1;
      mainBase = 0;

      crossSize = 'width';
      crossStart = 'left';
      crossEnd = 'right';
      break;
    }

    case 'column-reverse': {
      mainSize = 'height';
      mainStart = 'bottom';
      mainEnd = 'top';
      mainSign = -1;
      mainBase = style[mainSize];

      crossSize = 'width';
      crossStart = 'left';
      crossEnd = 'right';
      break;
    }

    default: {
      throw new Error(`Unexpected flexDirection value: ${style['flex-direction']}`);
    }
  }

  if (style['flex-wrap'] === 'wrap-reverse') {
    crossSign = -1;
    crossBase = style[crossSize];
    [crossStart, crossEnd] = [crossEnd, crossStart];
  } else {
    crossSign = 1;
    crossBase = 0;
    [crossStart, crossEnd] = [crossStart, crossEnd];
  }

  if (style[mainSize] == null) {
    style[mainSize] = 0;
    
    for (let i = 0; i < items.length; i++) {
      const itemStyle = getStyle(items[i]);
      if (itemStyle[mainSize] != null) {
        style[mainSize] += itemStyle[mainSize];
      }
    }
  }

  let flexLine: {
    line: Element[],
    mainSpace: number,
    crossSpace: number,
  } = {
    line: [],
    mainSpace: 0,
    crossSpace: 0,
  };

  const flexLines = [flexLine];

  // 主轴剩余空间
  let mainSpace: number = style[mainSize];

  // 交叉轴剩余空间
  let crossSpace: number = 0;

  // Collect flex items into flex lines
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    const itemStyle = getStyle(item);

    if (itemStyle[mainSize] == null) {
      itemStyle[mainSize] = 0;
    }

    if (itemStyle.flex != null) {
      flexLine.line.push(item);
      continue;
    }

    // If the flex container is single-line,
    // collect all the flex items into a single flex line.
    if (style['flex-wrap'] === 'nowrap') {
      mainSpace -= itemStyle[mainSize];

      if (itemStyle[crossSize] != null) {
        crossSpace = Math.max(crossSpace, itemStyle[crossSize]);
      }

      flexLine.line.push(item);
      continue;
    }

    // If the very first uncollected item wouldn’t fit,
    // collect just it into the line.
    if (itemStyle[mainSize] > style[mainSize]) {
      itemStyle[mainSize] = style[mainSize];
    }

    // Otherwise, starting from the first uncollected item,
    // collect consecutive items one by one until the first
    // time that the next collected item would not fit into
    // the flex container’s inner main size.
    if (itemStyle[mainSize] > mainSpace) {
      flexLine.mainSpace = mainSpace;
      flexLine.crossSpace = crossSpace;

      flexLine = {
        line: [item],
        mainSpace: 0,
        crossSpace: 0,
      };

      flexLines.push(flexLine);

      mainSpace = style[mainSize];
      crossSpace = 0;
    } else {
      flexLine.line.push(item);
    }

    // Find the largest outer hypothetical cross size.
    if (itemStyle[crossSize] != null) {
      crossSpace = Math.max(crossSpace, itemStyle[crossSize])
    }

    mainSpace -= itemStyle[mainSize];
  }

  flexLine.mainSpace = mainSpace;

  // If the flex container is single-line and has a definite cross size,
  // the cross size of the flex line is the flex container’s inner cross size.
  if (style['flex-wrap'] === 'nowrap') {
    flexLine.crossSpace = style[crossSize] ?? crossSpace;
  } else {
    flexLine.crossSpace = crossSpace;
  }

  // 9.7. Resolving Flexible Lengths

  // 9.7.1 Determine the used flex factor.
  // Sum the outer hypothetical main sizes of all items on the line.
  // If the sum is less than the flex container’s inner main size,
  // use the flex grow factor for the rest of this algorithm;
  // otherwise, use the flex shrink factor.

  if (mainSpace < 0) {
    const scale = style[mainSize] / (style[mainSize] - mainSpace);

    let currentMain = mainBase;
    for (let i = 0; i < items.length; i++) {
      const item = items[i];
      const itemStyle = getStyle(item);

      if (itemStyle.flex) {
        itemStyle[mainSize] = 0;
      }

      /**
       *     |<---+ mainSize +--->|
       *     +--------------------+
       *     |                    |
       *     |                    |
       *     |                    |
       *     |                    |
       *     +--------------------+
       *  mainStart             mainEnd
       *
       */

      itemStyle[mainSize] *= scale;

      itemStyle[mainStart] = currentMain;
      itemStyle[mainEnd] = itemStyle[mainStart] + mainSign * itemStyle[mainSize];

      currentMain = itemStyle[mainEnd];
    }
  } else {
    for (const flexLine of flexLines) {
      let flexTotal: number = 0;

      // Find the ratio of the item’s flex grow factor to the sum of
      // the flex grow factors of all unfrozen items on the line. 
      for (const item of flexLine.line) {
        const itemStyle = getStyle(item);

        if (itemStyle.flex != null) {
          flexTotal += itemStyle.flex;
        }
      }

      // Set the item’s target main size to its flex base size
      // plus a fraction of the remaining free space proportional to the ratio.
      const mainSpace = flexLine.mainSpace;

      if (flexTotal > 0) {
        let currentMain = mainBase;

        for (const item of flexLine.line) {
          const itemStyle = getStyle(item);

          if (itemStyle.flex) {
            itemStyle[mainSize] = (mainSpace / flexTotal) * itemStyle.flex;
          }

          itemStyle[mainStart] = currentMain;
          itemStyle[mainEnd] = itemStyle[mainStart] + mainSign * itemStyle[mainSize];

          currentMain = itemStyle[mainEnd];
        }
      } else {
        // 9.5. Main-Axis Alignment
        // Distribute any remaining free space. For each flex line:
        // Align the items along the main-axis per justify-content.
        let currentMain: number;
        let step: number;

        switch (style['justify-content']) {
          case 'flex-start': {
            currentMain = mainBase;
            step = 0;
            break;
          }

          case 'flex-end': {
            currentMain = mainBase + mainSpace * mainSign;
            step = 0;
            break;
          }

          case 'center': {
            currentMain = mainBase + mainSpace * mainSign / 2;
            step = 0;
            break;
          }

          case 'space-between': {
            step = mainSpace / (flexLine.line.length - 1) * mainSign;
            currentMain = mainBase;
            break;
          }

          case 'space-around': {
            step = mainSpace / flexLine.line.length * mainSign;
            currentMain = mainBase + step / 2;
            break;
          }

          default: {
            throw new Error(`Unexpected justify-content value: ${style['justify-content']}`);
          }
        }

        for (let i = 0; i < flexLine.line.length; i++) {
          const item = flexLine.line[i];
          const itemStyle = getStyle(item);

          itemStyle[mainStart] = currentMain;
          itemStyle[mainEnd] = itemStyle[mainStart] + mainSign * itemStyle[mainSize];

          currentMain = itemStyle[mainEnd] + step;
        }
      }
    }
  }

  // 9.6. Cross-Axis Alignment
  if (style[crossSize] == null) {
    // 交叉轴大小自适应，剩余的空间为 0
    crossSpace = 0;

    style[crossSize] = 0;
    for (let i = 0; i < flexLines.length; i++) {
      style[crossSize] += flexLines[i].crossSpace;
    }
  } else {
    crossSpace = style[crossSize];
    for (let i = 0; i < flexLines.length; i++) {
      crossSpace -= flexLines[i].crossSpace;
    }
  }

  // Align all flex items along the cross-axis per align-self.
  // const lineSize = style[crossSize] / flexLines.length;

  let step: number;
  let currentCross: number;

  // Only multi-line flex containers ever have free space in the cross-axis for lines to be aligned in,
  // because in a single-line flex container the sole line automatically stretches to fill the space.
  if (flexLines.length == 1) {
    style['align-content'] = 'stretch';
  }

  switch (style['align-content']) {
    case 'flex-start': {
      currentCross = crossBase;
      step = 0;
      break;
    }

    case 'flex-end': {
      currentCross = crossBase + crossSign * crossSpace;
      step = 0;
      break;
    }

    case 'center': {
      currentCross = crossBase + crossSign * crossSpace / 2;
      step = 0;
      break;
    }

    case 'space-between': {
      step = crossSpace / (flexLines.length - 1);
      currentCross = crossBase;
      break;
    }

    case 'space-around': {
      step = mainSpace / flexLine.line.length;
      currentCross = crossBase + crossSign * step / 2;
      break;
    }

    case 'stretch': {
      currentCross = crossBase;
      step = 0;
      break;
    }

    default: {
      throw new Error(`Unexpected align-content value: ${style['align-content']}`);
    }
  }

  for (const flexLine of flexLines) {
    const lineCrossSize = style['align-content'] === 'stretch'
      ? flexLine.crossSpace + crossSpace / flexLines.length
      : flexLine.crossSpace;

    for (let i = 0; i < flexLine.line.length; i++) {
      const item = flexLine.line[i];
      const itemStyle = getStyle(item);

      const align = itemStyle['align-self'] ?? style['align-items'];

      if (itemStyle[crossSize] == null) {
        itemStyle[crossSize] = align === 'stretch' ? lineCrossSize : 0;
      }

      switch (align) {
        case 'flex-start': {
          itemStyle[crossStart] = currentCross;
          itemStyle[crossEnd] = itemStyle[crossStart] + crossSign * itemStyle[crossSize];
          break;
        }

        case 'flex-end': {
          itemStyle[crossEnd] = currentCross + crossSign * lineCrossSize;
          itemStyle[crossStart] = itemStyle[crossEnd] - crossSign * itemStyle[crossSize];
          break;
        }

        case 'center': {
          itemStyle[crossStart] = currentCross + crossSign * (lineCrossSize - itemStyle[crossSize]) / 2;
          itemStyle[crossEnd] = itemStyle[crossStart] + crossSign * itemStyle[crossSize];
          break;
        }

        case 'stretch': {
          itemStyle[crossStart] = currentCross;
          itemStyle[crossEnd] = itemStyle[crossStart] + crossSign * itemStyle[crossSize];
          break;
        }
      }
    }
    currentCross += crossSign * (lineCrossSize + step);
  }
}

/**
 * 样式预处理（解析 CSS 属性的值）
 * @param element HTML 元素
 */
export function getStyle(element: Element) {
  if (element.style == null) {
    element.style = {};
  }

  for (const prop in element.computedStyle) {
    const { value } = element.computedStyle[prop];
    
    element.style[prop] = value;

    if (value.toString().match(/px$/)) {
      element.style[prop] = parseInt(value);
    }

    if (value.toString().match(/^[0-9\.]+$/)) {
      element.style[prop] = parseInt(value);
    }
  }

  return element.style;
}